package org.biogroovy.util;

import groovy.util.logging.Slf4j;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * This class determines the number of times that a given key occurs.
 * 
 * @param <K>
 *            The key type
 */
public class FrequencyMap<K> {

	/**
	 * A map containing keys, and the frequencies (counts) that those keys
	 * occur.
	 */
	private SortedMap<K, Integer> freqMap = null;

	/**
	 * Constructor.
	 */
	public FrequencyMap() {
		freqMap = new TreeMap<K, Integer>();
	}

	/**
	 * Convenience method for adding multiple keys to the map.
	 * 
	 * @param keys
	 *            the keys to be added.
	 */
	public void addAll(K... keys) {
		for (K key : keys) {
			add(key);
		}
	}

	/**
	 * Convenience method for adding multiple keys to the map.
	 * 
	 * @param keys
	 *            the keys to be added.
	 */
	public void addAll(List<K> keys) {
		for (K key : keys) {
			add(key);
		}
	}

	/**
	 * Convenience method for adding multiple keys to the map.
	 * 
	 * @param keys
	 *            the keys to be added.
	 */
	public void addAll(Collection<K> keys) {
		for (K key : keys) {
			add(key);
		}
	}

	/**
	 * Adds a key to the frequency map.
	 * 
	 * @param key
	 *            the key to be added.
	 */
	public void add(K key) {
		int count = getCount(key);
		count++;
		freqMap.put(key, count);
	}

	/**
	 * Removes a key from the frequency map if the count of occurrences &lt;= 0,
	 * otherwise it decrements the count of occurrences for that specific key.
	 * 
	 * @param key
	 *            the key of the object to be removed.
	 */
	public void remove(K key) {
		int count = getCount(key);
		count--;
		if (count <= 0) {
			removeAll(key);
		} else {
			freqMap.put(key, count);
		}
	}

	/**
	 * Removes the entry for the specified key.
	 * 
	 * @param key
	 *            the key to be removed.
	 */
	public void removeAll(K key) {
		freqMap.remove(key);
	}

	/**
	 * Gets a set of the entries in the frequency map. These entries should be
	 * sorted by the frequency of the entries.
	 * 
	 * @return an set of entries in sorted order.
	 */
	public Set<Entry<K, Integer>> entrySet() {
		CountComparator comparator = new CountComparator(freqMap);

		SortedMap<K, Integer> sortedMap = new TreeMap<K, Integer>(comparator);
		sortedMap.putAll(freqMap);
		return sortedMap.entrySet();
	}

	/**
	 * Gets a frequency/count of the number of times a key was added to the map.
	 * 
	 * @param key
	 *            the key that you want to query.
	 * @return a count of the frequency of a particular key.
	 */
	public int getCount(K key) {
		Integer count = freqMap.get(key);
		if (count == null) {
			return 0;
		} else {
			return count;
		}
	}

	/**
	 * Gets the total of all of the frequencies in the map.
	 * 
	 * @return the total of all of the frequencies in the map.
	 */
	public int getTotalCount() {
		int count = 0;
		for (Entry<K, Integer> entry : freqMap.entrySet()) {
			count += entry.getValue();
		}
		return count;
	}

	/**
	 * Gets the maximum frequency within the map.
	 * 
	 * @return the maximum frequency within the map
	 */
	public int getMaxCount() {
		return Collections.max(freqMap.values());
	}

	/**
	 * Gets the minimum frequency within the map.
	 * 
	 * @return the minimum frequency within the map.
	 */
	public int getMinCount() {
		return Collections.min(freqMap.values());
	}

	/**
	 * This method gets the counts for the entries in the frequency map as
	 * percentages of a total count of entries. This is useful if you want to
	 * create a pie chart showing the counts as percentages of a whole.
	 * 
	 * @return a map containing the entry values and percentage values.
	 */
	public Map<K, Float> getEntriesAsPercentages() {
		Map<K, Float> map = new HashMap<>();

		int total = getTotalCount();

		for (Entry<K, Integer> entry : freqMap.entrySet()) {
			float count = (float)getCount(entry.getKey());
			float pct = (count / total) * 100.0f;
			map.put(entry.getKey(), pct);
		}

		return map;
	}

	/**
	 * Removes all entries from the frequency map.
	 */
	public void clear() {
		freqMap.clear();
	}

	/**
	 * Gets the number of entries in the frequency map.
	 * 
	 * @return the number of entries
	 */
	public int size() {
		return freqMap.size();
	}

	/**
	 * Determines if the frequency map is empty.
	 * 
	 * @return true if the frequency map is empty.
	 */
	public boolean isEmpty() {
		return freqMap.isEmpty();
	}

	@Override
	public String toString() {
		return freqMap.toString();
	}

	/**
	 * Gets the frequency map.
	 */
	public Map<K, Integer> getMap() {
		return freqMap;
	}

	/**
	 * Compares the frequency counts for two entries in the map. Returns values
	 * in highest to lowest order.
	 */
	class CountComparator implements Comparator<K> {

		private Map<K, Integer> freqMap = null;

		/**
		 * Constructor.
		 * 
		 * @param map
		 *            the frequency map
		 */
		public CountComparator(Map<K, Integer> map) {
			this.freqMap = map;
		}

		@Override
		public int compare(K key1, K key2) {
			Integer count1 = this.freqMap.get(key1);
			Integer count2 = this.freqMap.get(key2);

			int comp = 0;

			if (count1 != null && count2 != null) {
				comp = count1.compareTo(count2);
			} else if (count1 == null) {
				comp = -1;
			} else if (count2 == null) {
				comp = 1;
			}

			return -1 * comp;
		}
	}
}
